package tracer;


import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.jdom.JDOMException;


import com.sun.jdi.Bootstrap;
import com.sun.jdi.VirtualMachine;
import com.sun.jdi.connect.Connector;
import com.sun.jdi.connect.Connector.Argument;
import com.sun.jdi.connect.IllegalConnectorArgumentsException;
import com.sun.jdi.connect.LaunchingConnector;
import com.sun.jdi.connect.VMStartException;


public class Trace {
	
    // Running remote VM
    private final VirtualMachine vm;
    // Thread transferring remote error stream to our error stream
    private Thread errThread = null;

    // Thread transferring remote output stream to our output stream
    private Thread outThread = null;

    // Class patterns for which we don't want events
    private String[] excludes = {"java.*", "javax.*", "sun.*", "com.sun.*"};
    
    public Trace(String target, String id, String workDir, boolean all) {
    	vm = launchTarget(target, "\"" + workDir + "\"");
    	generateTrace(id, workDir, all);
   	}
    
    /**
     * Generate the trace.
     * Enable events, start thread to display events, 
     * start threads to forward remote error and output streams,
     * resume the remote VM, wait for the final event, and shutdown.
     * 
     * @param all If this paramaters is false only live objects will be considered.
     */
    void generateTrace(String id, String workDir, boolean all) {
        final EventThread eventThread = new EventThread(vm, excludes, id, workDir, all);
        eventThread.setEventRequests();
        
        new Thread() {
        	public void run() {
                eventThread.start();
                redirectOutput();
                vm.resume();

                // Shutdown begins when event thread terminates
        		try {
        		    eventThread.join();
        		    errThread.join(); // Make sure output is forwarded 
        		    outThread.join(); // before we exit
        		} catch (InterruptedException exc) {
        		    // we don't interrupt
        		}
        	}
        }.start();
        
    	int execResult = Integer.MIN_VALUE;
		for (int i = 0; i < 6; i++) {
			try {
				Thread.sleep(5000);
				execResult = vm.process().exitValue();
				break;
			} catch (IllegalThreadStateException e) {
				// Process has not yet terminated. This is no problem,
				// we just continue waiting.
			} catch (InterruptedException e) {
			}
		}

		if (execResult == Integer.MIN_VALUE) {
			vm.suspend();
		}
    }   

    /**
     * Launch target VM.
     * Forward target's output and error.
     */
    VirtualMachine launchTarget(String mainArgs, String classpath) {
    	LaunchingConnector connector = findLaunchingConnector();
		Map<String, Argument> arguments = connectorArguments(connector, mainArgs, "-cp " + classpath);
        try {
        	return connector.launch(arguments);
        } catch (IOException exc) {
            throw new Error("Unable to launch target VM: " + exc);
        } catch (IllegalConnectorArgumentsException exc) {
        	throw new Error("Internal error: " + exc);
        } catch (VMStartException exc) {
        	throw new Error("Target VM failed to initialize: " + exc.getMessage());
        } catch (Exception e) {
        	throw new Error("Exception: " + e.getMessage());
        }
    }

    void redirectOutput() {
        Process process = vm.process();

        // Copy target's output and error to our output and error.
        errThread = new StreamRedirectThread("error reader", process.getErrorStream(), System.err);
        outThread = new StreamRedirectThread("output reader", process.getInputStream(), System.out);
        errThread.start();
        outThread.start();
    }
    
    /**
     * Find a com.sun.jdi.CommandLineLaunch connector
     */
    LaunchingConnector findLaunchingConnector() {
        List<Connector> connectors = Bootstrap.virtualMachineManager().allConnectors();
        Iterator<Connector> iter = connectors.iterator();
        while (iter.hasNext()) {
            Connector connector = (Connector)iter.next();
            if (connector.name().equals("com.sun.jdi.CommandLineLaunch")) {
                return (LaunchingConnector)connector;
            }
        }
        throw new Error("No launching connector");
    }

    /**
     * Return the launching connector's arguments.
     */
    Map<String, Argument> connectorArguments(LaunchingConnector connector, String mainArgs) {
        Map<String, Argument> arguments = connector.defaultArguments();
        Connector.Argument mainArg = 
	                   (Connector.Argument)arguments.get("main");
        if (mainArg == null) {
            throw new Error("Bad launching connector");
        }
        mainArg.setValue(mainArgs);
           
        return arguments;
    }
    
    /**
     * Wenn zustzlich zu der Zielklasse noch Optionen, blicherweise
     * der classpath angegeben wird, dann wird diese Methode aufgerufen
     * 
     * (Fr die Einbindung in den Jack ist dies relevant)
     * 
     * @param connector
     * @param mainArgs
     * @param optionArgs
     * @return
     */
    Map<String, Argument> connectorArguments(LaunchingConnector connector, String mainArgs, String optionArgs) {
        Map<String, Argument> arguments = connectorArguments(connector, mainArgs);
        
        Connector.Argument optionArg = (Connector.Argument)arguments.get("options");
        optionArg.setValue(optionArgs);
        
        return arguments;
    }
    
    
//    /**
//     * Mglicher Aufruf: "java kovida.Trace <Zielklasse>"
//     * oder "java kovida.Trace -all <Zielklasse>"
//     * Mit der -all option werden auch noch die Objekte ausgegeben, auf die es
//     * keine Referenz mehr gibt, aber die noch nicht vom GC entfernt wurden
//     * 
//     * @param args
//     */
//	public static void main(String[] args) {
//		try {
//			Settings.readConfig("kovida/conf.xml");
//		} catch (JDOMException e) {
//			e.printStackTrace();
//		} catch (IOException e) {
//			e.printStackTrace();
//		}
//		if(args[0].equals("-all")) {
//			new Trace(args[1], args[2], true);
//		}
//		else {
//			new Trace(args[0], args[1], false);
//		}
//	}
	
	public static void start(String target, String id, String workDir, boolean all, String config) {
		try {
			Settings.readConfig(config);
		} catch (JDOMException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

		new Trace(target, id, workDir, all);
	}
    
}
